/**
 * XssFilterUtil.java
 * com.sftz.framework.util
 * Copyright (c) 2018, 北京科技有限公司版权所有.
*/

package com.sftz.framework.util;

import org.apache.commons.lang.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.safety.Whitelist;
import org.jsoup.select.Elements;
import org.owasp.esapi.ESAPI;

import com.uxuexi.core.common.util.Util;

/**
 * 
 * 跨站点脚本过滤工具
 * <p>
 * 实现思路：
 * 一，对没有使用富文本编辑器的地方直接过滤成纯文本，不允许出现html:
 * pureText()
 * 
 * 二，使用了富文本的地方
 * 
 * 1,先使用jsoup的白名单ralaxed过滤一遍
 * 使用jsoup获取html文档标签:
 * 2，HTML标签之间的内容(<p>这里是内容</p>)使用ESAPI进行HTML Entity编码:
 * 		ESAPI.encoder().encodeForHTML(content);
 * 3，对标签的属性使用HTML属性编码:
 * 		ESAPI.encoder().encodeForHTMLAttribute(attrValue);
 * 4,提交的数据不允许出现script标签
 * 5,style属性的值，进行CSS编码
 * 		ESAPI.encoder().encodeForCSS(style);
 * 6,URL编码，href和src?
 * @author   朱晓川
 * @Date	 2018年3月3日 	 
 */
public class XssFilterUtil {

	/**
	 * jsoup白名单过滤器:
	 * 
	1)： none()
	该API会 清除所有HTML标签 ，仅保留文本节点。

	2)： simpleText()
	该API仅会 保留 b, em, i, strong, u 标签，除此之外的 所有HTML标签都会被清除 。

	3)： basic()
	该API会 保留 a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul 和其适当的属性 标签，除此之外的 所有HTML标签都会被清除，且该API不允许出现图片 (img tag) 。另外该API中允许出现的超链接中可以允许其指定 http, https, ftp, mailto 且在超链接中强制追加 rel=nofollow 属性。

	4)： basicWithImages()
	该API在 保留 basic() 中允许出现的标签的 同时也允许 出现图片 (img tag) 和img的相关适当属性，且其 src 允许其指定 http 或 https 。

	5)： relaxed()
	该API仅会 保留 a, b, blockquote, br, caption, cite, code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub, sup, table, tbody, td, tfoot, th, thead, tr, u, ul 标签，除此之外的 所有HTML标签都会被清除 ，且在超链接中 不会 强制追加 rel=nofollow 属性 。
	 */
	private final static Whitelist RELAXED_LIST = Whitelist.relaxed();
	private final static String[] SAFE_TAGS = new String[] { "embed", "object", "param", "span", "div", "input" };
	static {
		//添加可信标签到白名单
		RELAXED_LIST.addTags(SAFE_TAGS);

		/**
		 * 针对可信标签，添加可信属性。
		 * <p>
		 * 注意:这里千万要小心!!!
		 * 一旦添加了可信属性以后，属性的值在使用的时候必须经过encodeForHTMLAttribute处理,否则就有安全隐患。
		 * :all    -->    "style", "class", "id", "name"
		 * object  -->    "width", "height", "classid", "codebase"
		 * param   -->    "name", "value"
		 * embed   -->    "src", "quality", "width", "height", "allowFullScreen","allowScriptAccess", "flashvars", "name", "type", "pluginspage"
		 */

		//给所有标签添加可信属性,慎用
		RELAXED_LIST.addAttributes(":all", "style", "class", "id", "name");
		//给object标签添加可信属性
		RELAXED_LIST.addAttributes("object", "width", "height", "classid", "codebase");
		RELAXED_LIST.addAttributes("param", "name", "value");
		RELAXED_LIST.addAttributes("embed", "src", "quality", "width", "height", "allowFullScreen",
				"allowScriptAccess", "flashvars", "name", "type", "pluginspage");
		RELAXED_LIST.addAttributes("input", "value");
	}

	/**
	 * 富文本内容过滤
	 */
	public static String relaxed(String html) {
		if (StringUtils.isBlank(html))
			return "";
		//先使用jsoup过滤一遍
		String candidate = Jsoup.clean(html, RELAXED_LIST);
		Document doc = Jsoup.parseBodyFragment(candidate);
		Elements allElements = doc.getAllElements();
		if (!Util.isEmpty(allElements)) {
			//过滤标签之间的内容
			for (Element e : allElements) {

				int cs = e.childNodeSize();
				if (cs > 1) {
					continue;
				}
				String text = e.ownText();
				if (!Util.isEmpty(text)) {
					//文本,直接使用pureText
					String safe = pureText(text);

					//text(str)会清空节点内部，所以需要重新append进去
					e.text(safe);
					e.append(e.children().html());
				}

				//标签的属性
				Attributes attributes = e.attributes();
				if (!Util.isEmpty(attributes)) {
					for (Attribute attr : attributes) {
						String attrVal = attr.getValue();
						if (!Util.isEmpty(attrVal)) {
							//如果属性里面包含脚本
							boolean isScriptedAttr = isScriptedAttributeValue(attrVal);
							if (isScriptedAttr) {
								String attrSafe = ESAPI.encoder().encodeForHTMLAttribute(attrVal);
								attr.setValue(attrSafe);
							}
						}
					}
				}
			}// end of for allElements loop
		}
		//img的src属性如果是相对路径(/*)，则会被过滤掉
		//网站的图片必须使用绝对路径(形如:http://image.sktcm.cn/c528610f-ad1e-4f0c-b65a-000868d69198.jpg)
		return doc.select("body").html();
	}

	/**
	 * 纯文本过滤,只保留数据中的纯文本部分
	 */
	public static String pureText(String html) {
		if (StringUtils.isBlank(html))
			return "";
		return Jsoup.clean(html, Whitelist.none());
	}

	/**
	 * 判断一个html标签的attr是否包含脚本
	 * Private method that determines if an attribute value is scripted
	 * (potentially loaded with an XSS attack vector).
	 * 
	 * @param attrVal	The value of the attribute
	 * @return "true" if the attribute is scripted. "false" if not.
	 */
	private static boolean isScriptedAttributeValue(String attrValue) {
		String attrVal = decode(attrValue);
		attrVal = attrVal.trim().toLowerCase();

		if (attrVal.contains("javascript:")) {
			return true;
		}
		if (attrVal.contains("mocha:")) {
			return true;
		}
		if (attrVal.contains("eval")) {
			return true;
		}
		if (attrVal.contains("vbscript:")) {
			return true;
		}
		if (attrVal.contains("livescript:")) {
			return true;
		}
		if (attrVal.contains("expression(")) {
			return true;
		}
		if (attrVal.contains("url(")) {
			return true;
		}
		if (attrVal.contains("&{")) {
			return true;
		}
		if (attrVal.contains("&#")) {
			return true;
		}
		return false;
	}

	/**
	 * 从文本中移除控制字符
	 * Private method to remove control characters from the value
	 * 
	 * @param value	The value being modified
	 * @return	The value free from control characters
	 */
	private static String decode(String value) {
		String val = value;
		val = val.replace("\u0000", "");
		val = val.replace("\u0001", "");
		val = val.replace("\u0002", "");
		val = val.replace("\u0003", "");
		val = val.replace("\u0004", "");
		val = val.replace("\u0005", "");
		val = val.replace("\u0006", "");
		val = val.replace("\u0007", "");
		val = val.replace("\u0008", "");
		val = val.replace("\u0009", "");
		val = val.replace("\n", "");
		val = val.replace("\u000B", "");
		val = val.replace("\u000C", "");
		val = val.replace("\r", "");
		val = val.replace("\u000E", "");
		val = val.replace("\u000F", "");
		val = val.replace("\u0010", "");
		val = val.replace("\u0011", "");
		val = val.replace("\u0012", "");
		val = val.replace("\u0013", "");
		val = val.replace("\u0014", "");
		val = val.replace("\u0015", "");
		val = val.replace("\u0016", "");
		val = val.replace("\u0017", "");
		val = val.replace("\u0018", "");
		val = val.replace("\u0019", "");
		val = val.replace("\u001A", "");
		val = val.replace("\u001B", "");
		val = val.replace("\u001C", "");
		val = val.replace("\u001D", "");
		val = val.replace("\u001E", "");
		val = val.replace("\u001F", "");
		return val;
	}

}